package claim.layer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import claim.fuzzy.ExponentMembershipFunction;
import claim.fuzzy.LinearMembershipFunction;
import claim.fuzzy.MembershipFunction;
import claim.fuzzy.MinusXPlus1ExponentMembershipFunction;
import claim.geom.Feature;
import claim.geom.GeometryException;
import claim.geom.PolygonFeature;
import claim.geoobj.Boundary;
import claim.geoobj.Buffer;
import claim.geoobj.ClassConfiguration;
import claim.geoobj.TransitionZone;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

/**
 * A layer containing a number of features.
 * 
 * @author <a href="mailto:christoph.kinkeldey@hcu-hamburg.de">Christoph
 *         Kinkeldey</a>
 * 
 */
public class FeatureLayer implements Layer {

	private static final Log LOG = LogFactory.getLog(Class.class.getName());

	public static final String BOUNDARY_CLASS_ID = "-1";

	private ClassConfiguration classConfig;
	private Map<String, PolygonFeature> featureMap;
	protected Map<String, Collection<PolygonFeature>> featureByClassMap;
	private Map<String, Collection<String>> adjacencyMap;
	protected Map<String, Collection<Buffer>> buffersMap;
	protected Map<String, Collection<TransitionZone>> transitionZonesMap;

	private LinearRing boundingBox;

	public FeatureLayer(Collection<PolygonFeature> features,
			ClassConfiguration classConfig) {
		if (features == null || features.size() == 0) {
			LOG.error("cannot create layer from empty features.");
		}
		this.classConfig = classConfig;
		this.featureMap = new HashMap<String, PolygonFeature>();
		this.featureByClassMap = new HashMap<String, Collection<PolygonFeature>>();
		this.adjacencyMap = new HashMap<String, Collection<String>>();
		this.buffersMap = new HashMap<String, Collection<Buffer>>();
		this.transitionZonesMap = new HashMap<String, Collection<TransitionZone>>();
		addFeatures(features);
	}
	
	public Feature getFeature(String id) {
		return this.featureMap.get(id);
	}

	public LinearRing getBoundingBox() {
		return boundingBox;
	}

	public Set<String> getClassNames() {
		return this.featureByClassMap.keySet();
	}

	public void getNeighbours(Feature feature) {
		this.adjacencyMap.get(feature.getId());
	}

	public double getBufferSize(String id0, String id1) {
		String objClass0 = featureMap.get(id0).getObjectClass();
		String objClass1 = featureMap.get(id1).getObjectClass();
		return classConfig.getBufferSize(objClass0, objClass1);
	}

	private void addFeatures(Collection<PolygonFeature> features) {
		LOG.info("adding features...");
		for (PolygonFeature feature : features) {
			if (feature.getClassName().length() > 0) {
				this.featureMap.put(feature.getId(), feature);
				addFeatureByClass(feature.getObjectClass(), feature);
			} else {
				LOG.error("Cannot add feature for it has no class name: "
						+ feature);
			}
		}
		computeBoundingBox();
		computeAdjacencies();
	}

	private void addFeatureByClass(String objectClass, PolygonFeature feature) {
		if (this.featureByClassMap.containsKey(objectClass)) {
			featureByClassMap.get(objectClass).add(feature);
		} else {
			Collection<PolygonFeature> featuresOfClass = new HashSet<PolygonFeature>();
			featuresOfClass.add(feature);
			featureByClassMap.put(objectClass, featuresOfClass);
		}
	}

	private void computeBoundingBox() {
		LOG.info("computing bounding boxes...");
		Geometry[] geometries = new Geometry[featureMap.size()];
		int i = 0;
		for (PolygonFeature feature : this.featureMap.values()) {
			// LOG.debug("...computing bounding box for feature " +
			// feature.getId() + ".");
			geometries[i++] = ((Geometry) feature.getOuterRing());
		}
		GeometryCollection allBoundaries = new GeometryCollection(geometries,
				new GeometryFactory());
		this.boundingBox = (LinearRing) ((Polygon) allBoundaries.getEnvelope())
				.getExteriorRing();
	}

	public Collection<Feature> getBuffersAsFeatures() {
		Collection<Feature> bufferFeatures = new ArrayList<Feature>();
		for (String objId : this.buffersMap.keySet()) {
			for (Buffer buffer : this.buffersMap.get(objId)) {
				bufferFeatures.add(buffer.getAsFeature());
			}
		}
		return bufferFeatures;
	}

	public Collection<Feature> getBuffersAsFeatures(String className) {
		Collection<Feature> bufferFeatures = new ArrayList<Feature>();
		Collection<PolygonFeature> featuresOfClass = featureByClassMap
				.get(className);
		if (featuresOfClass != null) {
			for (PolygonFeature feature : featuresOfClass) {
				for (Buffer buffer : this.buffersMap.get(feature.getId())) {
					bufferFeatures.add(buffer.getAsFeature());
				}
			}
		} else {
			LOG.error("no features for class '" + className + "'");
		}
		return bufferFeatures;
	}

	public void createTransitionZones() {
		LOG.info("creating transition zones...");
		for (String featureId : featureMap.keySet()) {
			Feature feature = featureMap.get(featureId);
			// LOG.debug("...creating transition zones for feature " +
			Collection<TransitionZone> zones = new HashSet<TransitionZone>();
			Collection<Buffer> buffers = buffersMap.get(featureId);
			for (Buffer buffer : buffers) {
				MembershipFunction membershipFunction = MembershipFunction.DEFAULT_MEMBERSHIP_FUNCTION;
				if (feature.getObjectClass().compareTo("A")==0) {
					// TODO
					membershipFunction = new LinearMembershipFunction();
//					membershipFunction =  new ExponentMembershipFunction(.5);
//					membershipFunction =  new ExponentMembershipFunction(2);
				} else {
					membershipFunction = new LinearMembershipFunction();
//					membershipFunction = new MinusXPlus1ExponentMembershipFunction(0.5);
//					membershipFunction = new MinusXPlus1ExponentMembershipFunction(2);
				}
				zones.add(new TransitionZone(featureId, buffer, membershipFunction));
			}
			addTransitionZones(feature.getObjectClass(), zones);
		}
	}
	
	public void addTransitionZones(String className,
			Collection<TransitionZone> zones) {
		if (this.transitionZonesMap.containsKey(className)) {
			transitionZonesMap.get(className).addAll(zones);
		} else {
			Collection<TransitionZone> zonesOfClass = new HashSet<TransitionZone>();
			zonesOfClass.addAll(zones);
			transitionZonesMap.put(className, zonesOfClass);
		}

	}
	
	public void addTransitionZone(String className,
			TransitionZone transitionZone) {
		if (this.transitionZonesMap.containsKey(className)) {
			transitionZonesMap.get(className).add(transitionZone);
		} else {
			Collection<TransitionZone> zonesOfClass = new HashSet<TransitionZone>();
			zonesOfClass.add(transitionZone);
			transitionZonesMap.put(className, zonesOfClass);
		}
	}

	public void createBuffers() {
		LOG.info("creating buffers...");
		int i = 1;
		for (String objId : this.featureMap.keySet()) {
			LOG.debug("...creating buffer for feature " + objId
					+ ". (" + (i++) + " of " + this.featureMap.keySet().size()
					+ ")");
			Collection<Buffer> buffers = createBuffers(this.featureMap.get(objId));
			this.buffersMap.put(objId, buffers);
		}
	}

	private List<Buffer> createBuffers(PolygonFeature feature) {
		List<Boundary> boundaries = splitUpBoundaries(feature);
		List<Buffer> buffers = new ArrayList<Buffer>();
		for (Boundary boundary : boundaries) {
			try {
				buffers.add(new Buffer(boundary));
			} catch (GeometryException e) {
				LOG.error(e, e);
			}
		}
		return buffers;
	}

	protected Map<String, Double> getBufferSizes(String objId) {
		Map<String, Double> bufferSizes = new HashMap<String, Double>();
		for (String otherId : adjacencyMap.get(objId)) {
			bufferSizes
					.put(otherId, getBufferSizeForObjectPair(objId, otherId));
		}
		return bufferSizes;
	}

	private Double getBufferSizeForObjectPair(String objId, String otherId) {
		if (featureMap.containsKey(objId) && featureMap.containsKey(otherId)) {
			String objClass0 = this.featureMap.get(objId).getObjectClass();
			String objClass1 = this.featureMap.get(otherId).getObjectClass();
			return classConfig.getBufferSize(objClass0, objClass1);
		} else {
			return classConfig.getBufferSize(BOUNDARY_CLASS_ID,
					BOUNDARY_CLASS_ID);
		}

	}

	private List<Boundary> splitUpBoundaries(PolygonFeature feature) {
		List<Boundary> boundaries = new ArrayList<Boundary>();
		Polygon polygon = feature.getPolygon();
		addBoundary(feature, boundaries, polygon.getExteriorRing(), feature
				.getId()
				+ "." + "0", true);
		// inner rings
		for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
			addBoundary(feature, boundaries, polygon.getInteriorRingN(i),
					feature.getId() + "." + (i + 1), false);
		}
		return boundaries;
	}

	private void addBoundary(PolygonFeature feature, List<Boundary> boundaries,
			Geometry geom, String boundaryId, boolean outer) {
		Boundary boundary = new Boundary(boundaryId);
		boundary.setOuter(outer);
		Collection<String> neighbourIDs = adjacencyMap.get(feature.getId());
		for (String neighbourId : neighbourIDs) {
			Geometry neighGeom;
			if (neighbourId.compareTo(BOUNDARY_CLASS_ID) == 0) {
				boundary.putBoundary(neighbourId, geom
						.intersection(boundingBox));
				boundary.putBufferSize(neighbourId, Buffer.DEFAULT_BUFFER_SIZE);
			} else {
				PolygonFeature neighbour = this.featureMap.get(neighbourId);
				neighGeom = (Polygon) neighbour.getPolygon();
				boundary.putBoundary(neighbourId, geom.intersection(neighGeom));
				boundary.putBufferSize(neighbourId, getBufferSizeForObjectPair(
						feature.getId(), neighbourId));
			}

		}
		boundaries.add(boundary);
	}

	private void computeAdjacencies() {
		LOG.info("computing topology...");
		Iterator<String> iter = this.featureMap.keySet().iterator();
		while (iter.hasNext()) {
			PolygonFeature feature = featureMap.get(iter.next());
			// LOG.debug("...computing topology for feature " + feature.getId()
			// + ".");
			Collection<String> ids = computeAdjacencies(feature);
			this.adjacencyMap.put(feature.getId(), ids);
		}
	}

	private Collection<String> computeAdjacencies(PolygonFeature feature) {
		Geometry geom = (Geometry) feature.getPolygon();

		Geometry boundary = geom.getBoundary();
		Collection<String> neighbours = new HashSet<String>();
		if (boundary.intersects(boundingBox)) {
			neighbours.add(BOUNDARY_CLASS_ID);
		}
		for (PolygonFeature otherFeature : this.featureMap.values()) {
			Geometry otherGeom = (Geometry) otherFeature.getPolygon();

			if (feature.equals(otherFeature)) {
				continue;
			} else if (geom.intersects(otherGeom)) {
				neighbours.add(otherFeature.getId());
			}
		}
		return neighbours;
	}

	public double getMembership(String objClass, double x, double y) {
		if (!this.transitionZonesMap.keySet().contains(objClass)) {
			LOG.debug("no transition zone for object class " + objClass);
		} else {
			// first check if point intersects with a transition zone
			Collection<TransitionZone> zones = this.transitionZonesMap
					.get(objClass);
			for (TransitionZone zone : zones) {
				if (zone.pointWithin(x, y)) {
					return zone.getMembership(x, y);
				}
			}

		}
		// secondly, check if it intersects with a feature
		if (!this.featureByClassMap.keySet().contains(objClass)) {
			LOG.debug("no features for object class " + objClass);
		} else {
			Point point = new GeometryFactory()
					.createPoint(new Coordinate(x, y));
			for (PolygonFeature featureInClass : this.featureByClassMap
					.get(objClass)) {
				if (point.intersects(featureInClass.getPolygon())) {
					return 1;
				}
			}
		}
		// no intersection
		return 0;
	}

}
